﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;

namespace lumo.display.object2D
{
    using UpdateAction_t = Action<List<Particle>, double, object>;
    using EmitParticleAction_t = Action<List<Particle>, object>;
    using ParticleEmitionConditionFunc_t = Func<double, object, bool>;
    using UpdateActionList_t = List<KeyValuePair<Action<List<Particle>, double, object>, object>>;
    using EmitParticleActionList_t = List<KeyValuePair<Action<List<Particle>, object>, object>>;
    using ParticleEmitionConditionFuncList_t = List<KeyValuePair<Func<double, object, bool>, object>>;
    using System.Collections;
    /// <summary>
    /// ParticleEmitter class.
    /// </summary>
    public abstract class ParticleEmitter : IObject2D, IPositionable2D, IUpdatable, IActivatable
    {
        /// <summary>Particle Update Action List</summary>
        private UpdateActionList_t UpdateActionList = new UpdateActionList_t();
        /// <summary>Particle Emition Action List</summary>
        private EmitParticleActionList_t EmitParticleActionList = new EmitParticleActionList_t();
        /// <summary>Particle Emition Action List</summary>
        private ParticleEmitionConditionFuncList_t ParticleEmitionConditionFuncList = new ParticleEmitionConditionFuncList_t();
        /// <summary>Particle list</summary>
        protected List<Particle> ParticleList = new List<Particle>();
        /// <summary>Emit particle condition</summary>
        protected bool EmitParticle = false;
        /// <summary>Texture2D of emitter</summary>
        public Texture2D Texture = null;

        #region Public Actions

        /// <summary>
        /// Add an update action.
        /// </summary>
        /// <param name="UpdateAction">The action to add</param>
        /// <param name="Data">The data</param>
        public void AddUpdateAction(UpdateAction_t UpdateAction, object Data = null)
        {
            UpdateActionList.Add(new KeyValuePair<UpdateAction_t, object>(UpdateAction, Data));
        }

        /// <summary>
        /// Add an EmitParticle action.
        /// </summary>
        /// <param name="EmitParticleAction">The action to add</param>
        /// <param name="Data">The data</param>
        public void AddEmitParticleAction(EmitParticleAction_t EmitParticleAction, object Data = null)
        {
            EmitParticleActionList.Add(new KeyValuePair<EmitParticleAction_t, object>(EmitParticleAction, Data));
        }

        /// <summary>
        /// Add an UpdateParticleEmition function.
        /// </summary>
        /// <param name="ParticleEmitionConditionFunc">The function to add</param>
        /// <param name="Data">The data</param>
        public void AddParticleEmitionConditionFunc(ParticleEmitionConditionFunc_t ParticleEmitionConditionFunc, object Data = null)
        {
            ParticleEmitionConditionFuncList.Add(new KeyValuePair<ParticleEmitionConditionFunc_t, object>(ParticleEmitionConditionFunc, Data));
        }

        /// <summary>
        /// Clear Particles.
        /// </summary>
        public void ClearParticles()
        {
            ParticleList.Clear();
        }

        /// <summary>
        /// Emit a single particle.
        /// </summary>
        public void Emit(Particle particle)
        {
            ParticleList.Add(particle);
        }

        #endregion

        #region Internal Actions

        #region Update Actions

        /// <summary>
        /// Update Particle Life;
        /// </summary>
        /// <param name="ParticleList">List of particles</param>
        /// <param name="Time">Time Passed</param>
        /// <param name="Data">Data Passed</param>
        public void UpdateLife(List<Particle> ParticleList, double Time, object Data)
        {
            float Timef = (float)Time;
            foreach (Particle particle in ParticleList)
            {
                particle.Life += Timef;
            }
        }

        /// <summary>
        /// Update Particle Position
        /// </summary>
        /// <param name="ParticleList">List of particles</param>
        /// <param name="Time">Time Passed</param>
        /// <param name="Data">Data Passed</param>
        public void UpdateLinearPosition(List<Particle> ParticleList, double Time, object Data)
        {
            float Timef = (float)(Time / 1000.0f);
            foreach (Particle particle in ParticleList)
            {
                particle.LinearPosition += Vector2.Multiply(particle.LinearVelocity, Timef);
            }
        }

        /// <summary>
        /// Update Particle Velocity
        /// </summary>
        /// <param name="ParticleList">List of particles</param>
        /// <param name="Time">Time Passed</param>
        /// <param name="Data">Data Passed</param>
        public void UpdateLinearVelocity(List<Particle> ParticleList, double Time, object Data)
        {
            float Timef = (float)(Time / 1000.0f);
            foreach (Particle particle in ParticleList)
            {
                particle.LinearVelocity += Vector2.Multiply(particle.LinearAcceleration, Timef);
            }
        }

        /// <summary>
        /// Update Particle Acceleration
        /// </summary>
        /// <param name="ParticleList">List of particles</param>
        /// <param name="Time">Time Passed</param>
        /// <param name="Data">Data Passed</param>
        public void UpdateLinearAcceleration(List<Particle> ParticleList, double Time, object Data)
        {
            float Timef = (float)(Time / 1000.0f);
            foreach (Particle particle in ParticleList)
            {
                particle.LinearAcceleration = Vector2.Multiply(particle.LinearForce, Timef);
            }
        }

        /// <summary>
        /// Update Particle Linear Movement
        /// </summary>
        /// <param name="ParticleList">List of particles</param>
        /// <param name="Time">Time Passed</param>
        /// <param name="Data">Data Passed</param>
        public void UpdateLinearMovement(List<Particle> ParticleList, double Time, object Data)
        {
            float Timef = (float)(Time / 1000.0f);
            foreach (Particle particle in ParticleList)
            {
                particle.LinearAcceleration = Vector2.Multiply(particle.LinearForce, Timef);
                particle.LinearVelocity += Vector2.Multiply(particle.LinearAcceleration, Timef);
                particle.LinearPosition += Vector2.Multiply(particle.LinearVelocity, Timef);
            }
        }

        /// <summary>
        /// Update Particle Rotation
        /// </summary>
        /// <param name="ParticleList">List of particles</param>
        /// <param name="Time">Time Passed</param>
        /// <param name="Data">Data Passed</param>
        public void UpdateRotation(List<Particle> ParticleList, double Time, object Data)
        {
            float Timef = (float)(Time / 1000.0f);
            foreach (Particle particle in ParticleList)
            {
                particle.Rotation += (particle.AngularVelocity * Timef);
            }
        }

        /// <summary>
        /// Update Particle Angular Velocity
        /// </summary>
        /// <param name="ParticleList">List of particles</param>
        /// <param name="Time">Time Passed</param>
        /// <param name="Data">Data Passed</param>
        public void UpdateAngularVelocity(List<Particle> ParticleList, double Time, object Data)
        {
            float Timef = (float)(Time / 1000.0f);
            foreach (Particle particle in ParticleList)
            {
                particle.AngularVelocity += (particle.AngularAcceleration * Timef);
            }
        }

        /// <summary>
        /// Update Particle Angular Acceleration
        /// </summary>
        /// <param name="ParticleList">List of particles</param>
        /// <param name="Time">Time Passed</param>
        /// <param name="Data">Data Passed</param>
        public void UpdateAngularAcceleration(List<Particle> ParticleList, double Time, object Data)
        {
            float Timef = (float)(Time / 1000.0f);
            foreach (Particle particle in ParticleList)
            {
                particle.AngularAcceleration = (particle.Torque * Timef);
            }
        }

        /// <summary>
        /// Update Particle Angular Acceleration
        /// </summary>
        /// <param name="ParticleList">List of particles</param>
        /// <param name="Time">Time Passed</param>
        /// <param name="Data">Data Passed</param>
        public void UpdateAngularMovement(List<Particle> ParticleList, double Time, object Data)
        {
            float Timef = (float)(Time / 1000.0f);
            foreach (Particle particle in ParticleList)
            {
                particle.AngularAcceleration = (particle.Torque * Timef);
                particle.AngularVelocity += (particle.AngularAcceleration * Timef);
                particle.Rotation += (particle.AngularVelocity * Timef);
            }
        }

        /// <summary>
        /// Update Particle Movement
        /// </summary>
        /// <param name="ParticleList">List of particles</param>
        /// <param name="Time">Time Passed</param>
        /// <param name="Data">Data Passed</param>
        public void UpdateMovement(List<Particle> ParticleList, double Time, object Data)
        {
            float Timef = (float)(Time / 1000.0f);
            foreach (Particle particle in ParticleList)
            {
                particle.LinearAcceleration = Vector2.Multiply(particle.LinearForce, Timef);
                particle.LinearVelocity += Vector2.Multiply(particle.LinearAcceleration, Timef);
                particle.LinearPosition += Vector2.Multiply(particle.LinearVelocity, Timef);

                particle.AngularAcceleration = (particle.Torque * Timef);
                particle.AngularVelocity += (particle.AngularAcceleration * Timef);
                particle.Rotation += (particle.AngularVelocity * Timef);
            }
        }

        /// <summary>
        /// Update Particle Movement
        /// </summary>
        /// <param name="ParticleList">List of particles</param>
        /// <param name="Time">Time Passed</param>
        /// <param name="Data">Data Passed</param>
        public void UpdateColorFadeInOut(List<Particle> ParticleList, double Time, object Data)
        {
            Type DataType = Data.GetType();
            Type ExpectedType = typeof(float);

            if (DataType.IsArray && ExpectedType.IsAssignableFrom(DataType.GetElementType()))
            {
                float[] array = Data as float[];
                if (array.Length < 2)
                    throw new Exception("Invalid argument for array in UpdateColorFadeInOut");
                float FadeInTime = array[0];
                float FadeOutTime = array[1];
                float FadeFactor = ((array.Length >= 3) ? array[2] : 1);
                float DefaultOpacity = ((array.Length >= 4) ? array[3] : 1);
                if (FadeFactor != 1)
                {
                    foreach (Particle particle in ParticleList)
                    {
                        if (particle.Life < FadeInTime)
                            particle.Opacity = (float)Math.Pow(particle.Life / FadeInTime, FadeFactor) * DefaultOpacity;
                        else if (particle.Life > (particle.MaxLife - FadeOutTime))
                            particle.Opacity = (float)Math.Pow(((particle.MaxLife - particle.Life) / FadeOutTime), FadeFactor) * DefaultOpacity;
                        else
                            particle.Opacity = DefaultOpacity;
                    }
                }
                else
                {
                    foreach (Particle particle in ParticleList)
                    {
                        if (particle.Life < FadeInTime)
                            particle.Opacity = (particle.Life / FadeInTime) * DefaultOpacity;
                        else if (particle.Life > (particle.MaxLife - FadeOutTime))
                            particle.Opacity = (((particle.MaxLife - particle.Life) / FadeOutTime)) * DefaultOpacity;
                        else
                            particle.Opacity = DefaultOpacity;
                    }
                }
            }
        }

        /// <summary>
        /// Clear the forces.
        /// </summary>
        /// <param name="ParticleList">List of particles</param>
        /// <param name="Time">Time Passed</param>
        /// <param name="Data">Data Passed</param>
        public void UpdateClearForces(List<Particle> ParticleList, double Time, object Data)
        {
            foreach (Particle particle in ParticleList)
                particle.LinearForce = Vector2.Zero;
        }

        /// <summary>
        /// Update The Particles using a constant force
        /// </summary>
        /// <param name="ParticleList">List of particles</param>
        /// <param name="Time">Time Passed</param>
        /// <param name="Data">Data Passed</param>
        public void UpdateConstantForce(List<Particle> ParticleList, double Time, object Data)
        {
            Type DataType = Data.GetType();
            Type ExpectedType = typeof(Vector2);

            if (DataType == ExpectedType)
            {
                Vector2 Force = (Vector2)Data;
                foreach (Particle particle in ParticleList)
                    particle.LinearForce += Force;
            }
        }

        /// <summary>
        /// Update The Particles using a force
        /// </summary>
        /// <param name="ParticleList">List of particles</param>
        /// <param name="Time">Time Passed</param>
        /// <param name="Data">Data Passed</param>
        public void UpdateForce(List<Particle> ParticleList, double Time, object Data)
        {
            if (Data is Func<Particle, Vector2>)
            {
                Func<Particle, Vector2> ForceFunc = Data as Func<Particle, Vector2>;
                foreach (Particle particle in ParticleList)
                    particle.LinearForce += ForceFunc(particle);
            }
        }

        /// <summary>
        /// Update The Particles using a force
        /// </summary>
        /// <param name="ParticleList">List of particles</param>
        /// <param name="Time">Time Passed</param>
        /// <param name="Data">Data Passed</param>
        public void UpdateFriction(List<Particle> ParticleList, double Time, object Data)
        {
            float FrictionFactor = Convert.ToSingle(Data);
            if (FrictionFactor != 0)
            {
                float Timef = (float)(Time *  FrictionFactor);
                foreach (Particle particle in ParticleList)
                    particle.LinearForce += Vector2.Multiply(particle.LinearVelocity, Timef);
            }
        }

        #endregion

        #region Emit Actions

        /// <summary>
        /// Emit a single stopped particle.
        /// </summary>
        /// <param name="ParticleList">List of particles</param>
        /// <param name="Data">Data Passed</param>
        public void EmitStoppedParticle(List<Particle> ParticleList, object Data)
        {
            Particle P = new Particle();
            P.LinearPosition = this.Position;
            P.MaxLife = Convert.ToSingle(Data);
            ParticleList.Add(P);
        }

        /// <summary>
        /// Emit a single stopped particle.
        /// </summary>
        /// <param name="ParticleList">List of particles</param>
        /// <param name="Data">Data Passed</param>
        public void EmitCustomParticleAction(List<Particle> ParticleList, object Data)
        {
            Action<List<Particle>> ParticleAction = Data as Action<List<Particle>>;
            ParticleAction(ParticleList);
        }

        /// <summary>
        /// Emit a single stopped particle.
        /// </summary>
        /// <param name="ParticleAction">The action to make.</param>
        public void CustomEmitParticleAction(Action<List<Particle>> ParticleAction)
        {
            AddEmitParticleAction(EmitCustomParticleAction, ParticleAction);
        }

        #endregion

        #region Particle Emission Conditions

        /// <summary>
        /// Timer class.
        /// </summary>
        private class Timer
        {
            public float Current = 0;
            public float Max;
            public float Start = 0;
            public float Stop  = -1;
            public bool  Started = true;
            public bool  Stopped = false;
            public Timer(float Max) { this.Max = Max; }
        }

        /// <summary>
        /// Emit particle on timer.
        /// </summary>
        /// <param name="Time">Time passed</param>
        /// <param name="Data">Data used.</param>
        /// <returns>True if must emit.</returns>
        private bool EmitOnTimer(double Time, object Data)
        {
            Timer Timer = Data as Timer;
            if (Timer.Stopped)
                return false;
            float Timef = (float)Time;
            if (!Timer.Started)
            {
                Timer.Start -= Timef;
                if (Timer.Start <= 0)
                {
                    Timer.Started = true;
                    Timer.Current = 0;
                    return true;
                }
            }
            if (Timer.Stop > 0)
            {
                Timer.Stop -= Timef;
                if (Timer.Stop <= 0)
                {
                    Timer.Stopped = true;
                    return false;
                }
            }
            Timer.Current += Timef;
            if (Timer.Current >= Timer.Max)
            {
                while (Timer.Current >= Timer.Max)
                    Timer.Current -= Timer.Max;
                return true;
            }
            return false;
        }

        /// <summary>
        /// Emit a particle every X miliseconds
        /// </summary>
        /// <param name="Time">Time between emissions</param>
        public void EmitEveryXMilliseconds(float Time)
        {
            AddParticleEmitionConditionFunc(EmitOnTimer, new Timer(Time));
        }

        /// <summary>
        /// Emit a particle every X miliseconds for N miliseconds.
        /// </summary>
        /// <param name="Time">Time passed</param>
        /// <param name="Stop">Emission Period</param>
        public void EmitEveryXMillisecondsForNMilliseconds(float Time, float Stop)
        {
            Timer Timer = new Timer(Time);
            Timer.Stop = Stop;
            AddParticleEmitionConditionFunc(EmitOnTimer, Timer);
        }

        /// <summary>
        /// Emit a particle every X miliseconds after N miliseconds
        /// </summary>
        /// <param name="Time">Time passed</param>
        /// <param name="Start">Initial Wait Time</param>
        public void EmitEveryXMillisecondsAfterNMilliseconds(float Time, float Start)
        {
            Timer Timer = new Timer(Time);
            Timer.Start = Start;
            Timer.Started = false;
            AddParticleEmitionConditionFunc(EmitOnTimer, Timer);
        }

        /// <summary>
        /// Emit a particle every X miliseconds after N miliseconds for Y miliseconds.
        /// </summary>
        /// <param name="Time">Time passed</param>
        /// <param name="Start">Initial Wait Time</param>
        /// <param name="Stop">Emission Period</param>
        public void EmitEveryXMillisecondsAfterNMillisecondsForYMilliseconds(float Time, float Start, float Stop)
        {
            Timer Timer = new Timer(Time);
            Timer.Start = Start;
            Timer.Started = false;
            Timer.Stop = Stop;
            AddParticleEmitionConditionFunc(EmitOnTimer, Timer);
        }

        /// <summary>
        /// Emit a particle when move internal private.
        /// </summary>
        /// <param name="Time">Time passed</param>
        /// <param name="Data">Array of last coordinates</param>
        private bool EmitOnMovementPrivate(double Time, object Data)
        {
            float []array = (float[]) Data;

            if (array[0] != this.Position.X || array[1] != this.Position.Y)
            {
                array[0] = this.Position.X;
                array[1] = this.Position.Y;
                return true;
            }
            return false;            
        }

        /// <summary>
        /// Emit a particle once
        /// </summary>
        public void EmitOnMove()
        {
            AddParticleEmitionConditionFunc(EmitOnMovementPrivate, new float[] { this.Position.X, this.Position.Y } );
        }

        /// <summary>
        /// Emit a particle once.
        /// </summary>
        public void EmitOnce() { this.EmitParticle = true; }

        #endregion

        #endregion

        #region Internal Functions

        /// <summary>
        /// Emit particles.
        /// </summary>
        protected void EmitParticles()
        {
            foreach (var EmitParticleActionPair in EmitParticleActionList)
                EmitParticleActionPair.Key(ParticleList, EmitParticleActionPair.Value);
        }

        /// <summary>
        /// Update the particles.
        /// </summary>
        private void UpdateParticles(lumo.LumoComponent Game)
        {
            double Time = Game.GameTime.ElapsedGameTime.TotalMilliseconds;
            foreach (var UpdateActionPair in UpdateActionList)
                UpdateActionPair.Key(ParticleList, Time, UpdateActionPair.Value);
        }

        /// <summary>
        /// Update the particles.
        /// </summary>
        private void UpdateParticlesEmitionStatus(lumo.LumoComponent Game)
        {
            double Time = Game.GameTime.ElapsedGameTime.TotalMilliseconds;
            foreach (var UpdateParticleEmitionFuncPair in ParticleEmitionConditionFuncList)
                EmitParticle = (UpdateParticleEmitionFuncPair.Key(Time, UpdateParticleEmitionFuncPair.Value) || EmitParticle);
        }

        #endregion

        #region IObject2D

        /// <summary>
        /// Draw the particles.
        /// </summary>
        /// <param name="SpriteBatch">The sprite batch object.</param>
        public virtual void Draw(SpriteBatch SpriteBatch)
        {
            if (_Visible && (Texture != null))
            {
                Vector2 Origin = new Vector2(Texture.Width / 2.0f, Texture.Height / 2.0f);
                foreach (Particle particle in ParticleList)
                {
                    SpriteBatch.Draw(Texture, particle.LinearPosition, null, particle.Color * particle.Opacity, particle.Rotation, Origin, particle.Scale, SpriteEffects.None, 0f); 
                }
            }
        }


        /// <summary>Private draw order float.</summary>
        private float _DrawOrder = 0f;
        /// <summary>
        /// Simple implementation of draw order.
        /// </summary>
        public float DrawOrder { get { return _DrawOrder; } set { _DrawOrder = value; } }

        /// <summary>Private visible variable.</summary>
        private bool _Visible = true;        /// <summary>
        /// Simple implementation of Visible.
        /// </summary>
        public bool Visible { get { return _Visible; } set { _Visible = value; } }

        #endregion

        #region IPositionable2D

        /// <summary> 
        /// Coordinates where the object is drawn 
        /// </summary>
        private Vector2 _Position = Vector2.Zero;
        /// <summary>
        /// Implementation of Position. IPositionable2D.
        /// </summary>
        public Vector2 Position { get { return _Position; } set { _Position = value; } }
        /// <summary>
        /// Implementation of X. IPositionable2D.
        /// </summary>
        public float X { get { return _Position.X; } set { _Position.X = value; } }
        /// <summary>
        /// Implementation of Y. IPositionable2D.
        /// </summary>
        public float Y { get { return _Position.Y; } set { _Position.Y = value; } }

        #endregion

        #region IUpdatable

        /// <summary>
        /// Update the particle.
        /// </summary>
        /// <param name="Game"></param>
        public virtual void Update(LumoComponent Game)
        {
            // Check if active
            if (Active)
            {
                // Emit particles if have to.
                UpdateParticlesEmitionStatus(Game);
                if (EmitParticle)
                {
                    EmitParticles();
                    EmitParticle = false;
                }
            }

            // Update particles
            UpdateParticles(Game);

            // Remove dead particles
            ParticleList.RemoveAll(particle => !particle.Alive);
        }

        #endregion

        #region IActivatable

        /// <summary>Active variable</summary>
        private bool _Active = true;

        /// <summary>Check if one is active</summary>
        public bool Active { get { return _Active; } set { _Active = value; } }

        #endregion
    }
}
